﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="IrcChannel.cs" company="LeetSoftwerx">
//   Copyright ©2012 LeetSoftwerx
// </copyright>
// <summary>
//   Represents an IRC channel
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Irclib
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Configuration;
    using System.Diagnostics.Contracts;
    using System.Globalization;
    using System.Text;
    using System.Text.RegularExpressions;

    /// <summary>
    /// Represents an IRC channel
    /// </summary>
    /// <remarks>
    /// To create an IrcChannel use the <see cref="M:Irclib.IrcConnection.Join"/> method of <see cref="Irclib.IrcConnection"/>
    /// </remarks>
    /// <example>
    /// <code>
    /// using Irclib;
    ///     using System;
    /// 
    ///     class foo
    ///     {
    ///     static int main()
    ///     {
    ///     IrcConnection _connection = new SecureIrcConnection(serverUri, Encoding.ASCII, true, "foobar");
    ///     _connection.Connect();
    ///     _connection.Initialize();
    ///     // TODO Finish documentation
    ///     }
    ///     }
    /// </code>
    /// </example>
    /// <seealso cref="Irclib.IrcConnection"/>
    /// <seealso cref="Irclib.SecureIrcConnection"/>
    public sealed class IrcChannel
    {
        #region Constants and Fields

        /// <summary>
        ///   The received message format.
        /// </summary>
        private const string ReceivedMessageFormat = "{0}!{1} PRIVMSG {2} :";

        /* This regex string should match either a channel pattern or a nickname */

        /// <summary>
        ///   The valid channel pattern.
        /// </summary>
        /// <devdoc>
        /// Enforcement of nick length should be done at the UI level where we can warn the user that nicks
        /// longer than 9 characters may not be supported by the server
        /// </devdoc>
        private const string ValidChannelPattern =
            @"(?<=#|\+|![A-Z0-9]{1,5}|&)[\p{IsBasicLatin}-[\0\a\s\r\n\,\:]]{1,50}|(?<=[\w\[\]\\`_^\{\|}])[\w\d]*";

        /// <summary>
        ///   The _inbound message queue.
        /// </summary>
        private readonly Queue<string> inboundMessageQueue = new Queue<string>();

        /// <summary>
        ///   The message header.
        /// </summary>
        private readonly string messageHeader;

        /// <summary>
        ///   The message length.
        /// </summary>
        private readonly int messageLength;

        /// <summary>
        ///   The _outbound message queue.
        /// </summary>
        private readonly ObservableCollection<string> outboundMessageQueue = new ObservableCollection<string>();

        /// <summary>
        ///   The _parent.
        /// </summary>
        private readonly IIrcConnection parent;

        /// <summary>
        ///   The _channel name.
        /// </summary>
        private readonly string channelName;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="IrcChannel"/> class. Creates a new IrcChannel instance
        /// </summary>
        /// <param name="channelName">
        /// The channel or nick to connect to 
        /// </param>
        /// <param name="parent">
        /// the server on which to open the channel 
        /// </param>
        /// <exception cref="System.ArgumentException">
        /// Invalid Channel name is passed in
        /// </exception>
        internal IrcChannel(string channelName, IIrcConnection parent)
        {
            Contract.Requires(!string.IsNullOrWhiteSpace(channelName) && Regex.IsMatch(channelName, ValidChannelPattern));
            Contract.Requires(parent != null);
            /* validate the channel name... this will throw an argument exception
             * if it is invalid
             */
            new RegexStringValidator(ValidChannelPattern).Validate(channelName);

            /* Validate that the server is not null... this would be a very bad thing
             * as we need that to talk on a channel... like this one...
             */

            this.channelName = channelName;
            this.parent = parent;

            /* create a base version of the message as it will be received,
            * because this will be longer than the message we actually send..
            * we need to take into account how the server will disseminate it
            * which include the users hostname and username
            */
            this.messageHeader = string.Format(
                CultureInfo.InvariantCulture, 
                ReceivedMessageFormat, 
                this.parent.Nickname, 
                this.parent.HostName, 
                this.ChannelName);

            /* How long our message can be... in bytes
             * we are assuming that this won't be negative... if it is that will be a
             * major issue....
             */
            this.messageLength = IrcConnection.IrcMessageLength
                                 - this.parent.ServerEncoding.GetByteCount(this.messageHeader);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="IrcChannel"/> class. Constructor for the default server channel should NEVER be used otherwise
        /// </summary>
        /// <param name="parent">
        /// the server on which to open the channel 
        /// </param>
        internal IrcChannel(IIrcConnection parent)
        {
            Contract.Requires(parent != null);

            this.channelName = parent.ServerName;
            this.parent = parent;
        }

        #endregion

        #region Events

        /// <summary>
        ///   The _new message event.
        /// </summary>
        internal event EventHandler<PrivateMessageRecievedEventArgs> NewMessageEvent;

        /// <summary>
        ///   The _send message event.
        /// </summary>
        private event EventHandler SendMessageEvent;

        #endregion

        #region Public Properties

        /// <summary>
        ///  Gets the name of the channel
        /// </summary>
        public string ChannelName
        {
            get
            {
                return this.channelName;
            }
        }

        /// <summary>
        ///   Gets InbountMessageQueue.
        /// </summary>
        public IEnumerable<string> InbountMessageQueue
        {
            get
            {
                return this.inboundMessageQueue;
            }
        }

        #endregion

        #region Properties

        /// <summary>
        ///  Gets the outbound message queue
        /// </summary>
        internal IEnumerable<string> OutboundQueue
        {
            get
            {
                return this.outboundMessageQueue;
            }
        }

        /// <summary>
        ///   Gets the server the channel is associated with
        /// </summary>
        internal IIrcConnection Server
        {
            get
            {
                return this.parent;
            }
        }

        #endregion

        #region Public Methods and Operators

        /// <summary>
        /// Sends a message to this channel
        /// </summary>
        /// <param name="message">
        /// The message to send 
        /// </param>
        public void SendMessage(string message)
        {
            Contract.Requires(
                !string.IsNullOrWhiteSpace(message) && Regex.IsMatch(message, @"[^\r\n\0\p{Cc}]+"));

            /* because utf16 supports surrogate pairs... and we need to keep characters on
             * the same line we need to get them by glyph... not .Net character which
             * might be the upper or lower half of a surrogate pair, we also need to
             * normalize it, using form KC will ensure that we have the highest probabilty
             * that the information will be preserved
             */
            var currentString = new StringInfo(message.Normalize(NormalizationForm.FormKC));

            // indicates the beginning of the string
            int start = 0;

            // The location of the end of the current segment
            int end = this.messageLength < message.Length ? this.messageLength : message.Length;

            /* keep going until we have queued all portions of the message
             * HACK: this is a really bad from a memory use perspective
             * because it creates new strings like mad, however stringbuilder
             * doesn't provide the functionality we need to do this..
             */
            while (start < message.Length)
            {
                // The current fragment we are working on
                string originalFragment = currentString.SubstringByTextElements(start, end);
                var fragment = new StringBuilder(originalFragment);

                /* attempt to break it at a space, if this is -1 or more than 10
                 * characters from the end... like as not we are in an east Asian sentence
                 * and we should break at the character, realistically we should only try this
                 * once, if it doesn't work... break at the character
                 */
                int attemptedEnd = originalFragment.LastIndexOf(' ');
                if (end - 10 < attemptedEnd && attemptedEnd < end)
                {
                    fragment.Remove(attemptedEnd, end - attemptedEnd);
                    end = attemptedEnd;
                }

                while (this.parent.ServerEncoding.GetByteCount(fragment.ToString()) > this.messageLength)
                {
                    fragment.Insert(0, currentString.SubstringByTextElements(start, --end));
                    fragment.Length = end - start;
                }

                this.outboundMessageQueue.Add(this.messageHeader + fragment);
                start = end + 1;
                end += fragment.Length;
            }

            // send the message to the send thread in the IrcConnection
            if (this.SendMessageEvent != null)
            {
                this.SendMessageEvent.BeginInvoke(this, new EventArgs(), null, null);
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// The on message received.
        /// </summary>
        /// <param name="sender">
        /// The sender. 
        /// </param>
        /// <param name="e">
        /// The e. 
        /// </param>
        internal void OnMessageRecieved(object sender, PrivateMessageRecievedEventArgs e)
        {
            Contract.Requires(e != null);

            this.inboundMessageQueue.Enqueue(e.Message);

            // _bufferWriter.WriteLine(e.Message);
            if (this.NewMessageEvent != null)
            {
                this.NewMessageEvent(this, e);
            }
        }

        #endregion
    }
}